package org.iamd.hibernate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.Table;

import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import org.iamd.domain.business.ApplicationException;
import org.iamd.domain.data.DataAccessException;
import org.iamd.hibernate.dao.GenIdDAO;
import org.iamd.hibernate.vo.GenId;
import org.iamd.hibernate.vo.HibernateVO;
import org.iamd.util.Messenger;

/**
 * Клас, който генерира идентификатори на таблици. Идентификаторите се съхраняват в таблица, като
 * всеки ред представлява двойка име на таблица и последно генериран идентификатор. Класът е
 * singleton.
 * 
 * @author И. Атанасов
 */
public final class HibernateIdGenerator {

	/**
	 * Генератор на идентификатори за дадена таблица.
	 * 
	 * @author И. Атанасов
	 */
	private class TableIdGenerator implements Runnable {

		/** Следващият идентификатор за съответния VO. */
		private int currentId;

		/** Име на таблица. */
		private String tableName;

		/**
		 * Инициализация на съответните полета.
		 * 
		 * @param currentId
		 *            последно генериран идентификатор
		 * @param tableName
		 *            име на таблица
		 */
		private TableIdGenerator(int currentId, String tableName) {
			this.currentId = currentId;
			this.tableName = tableName;
		}

		/**
		 * За текущата таблица генерира следващ идентификатор и го връща. Генерирането на
		 * идентификатора става в отделна транзакция, затова ако пропадне записа на обекта -
		 * идентификатора се пропуска. Ако не съществува брояч за тази таблица - той се инициализира
		 * със стойност 1. Последно използваните идентификатори за всички таблици се записват в една
		 * таблица.
		 */
		@Override
		public void run() {
			synchronized (this) {
				GenIdDAO giDAO = null;
				try {
					giDAO = new GenIdDAO();
					giDAO.beginWork();
					Criteria crit = giDAO.createCriteria();
					crit.add(Restrictions.eq(GenIdDAO.TABLE_NAME, this.tableName));
					List<GenId> foundObjs = giDAO.findByCriteria(crit);
					GenId genId = null;
					this.currentId++;
					if (!foundObjs.isEmpty()) {
						genId = foundObjs.get(0);
						genId.setLastUsedId(this.currentId);
						giDAO.update(genId);
					} else {

						// инициализация на брояч за таблица
						genId = giDAO.createInstance();
						genId.setId(genIdTable.equals(this.tableName) ? this.currentId
								: HibernateIdGenerator.instance.generateId(GenId.class));
						genId.setTableName(this.tableName);
						genId.setLastUsedId(this.currentId);
						giDAO.save(genId);
					}
					giDAO.commitWork();
				} catch (ApplicationException e) {
					try {
						if (giDAO != null) {
							giDAO.rollback();
						}
					} catch (DataAccessException e1) {
						// TODO log message
						Messenger.logError(null, null);
					}
					// TODO log message
					Messenger.logError(null, null);
				}
			}
		}

	}

	/** Име на таблицата, в която се съхраняват броячите. */
	private static final String genIdTable = GenId.class.getAnnotation(Table.class).name();

	/** Единствената инстанция на класа. */
	private static HibernateIdGenerator instance;

	/**
	 * Връща съответната инстанция на класа - singleton pattern.
	 * 
	 * @return инстанция на генератора.
	 */
	public static HibernateIdGenerator getInstance() {
		if (instance == null) {
			instance = new HibernateIdGenerator();
		}
		return instance;
	}

	/** Мап с инстанции на генератори. */
	private Map<Class<? extends HibernateVO>, TableIdGenerator> tableGens;

	/**
	 * Конструктор. Инициализиране на списъка съдържащ генераторите за съответните таблици.
	 */
	private HibernateIdGenerator() {
		this.tableGens = new HashMap<Class<? extends HibernateVO>, TableIdGenerator>();
	}

	/**
	 * За текущият клас генерира следващ идентификатор и го връща. Генерирането на идентификатора
	 * става в отделна транзакция, т.е. ако пропадне записа на обекта - той се пропуска.
	 * 
	 * @param voClass
	 *            наследник на {@link HibernateVO} за който трябва да се генерира пореден
	 *            идентификатор
	 * 
	 * @return следващият генериран идентификатор за съответния VO или 0, ако входния параметър е
	 *         <code>null</code>, или не съществува такъв map-нат клас в Hibernate environment-a
	 * 
	 * @throws DataAccessException
	 *             има проблем при генерирането на ИД-то
	 */
	public int generateId(Class<? extends HibernateVO> voClass) throws DataAccessException {
		if (voClass != null) {
			TableIdGenerator generator = this.tableGens.get(voClass);
			if (generator != null) {
				try {
					Thread execThread = new Thread(generator);
					execThread.start();
					execThread.join();
				} catch (InterruptedException e) {
					// TODO throw exception
					throw new DataAccessException(null);
				}
				return generator.currentId;
			}
		}
		return 0;
	}

	/**
	 * Инициализира Генераторите на ИД-та за отделните таблици по зададените класове на value
	 * обектите. В случай, че няма ред в базата за даден генератор (нова таблица), генератора се
	 * инициализира с нулев брояч.
	 * 
	 * @see org.iamd.hibernate.vo.GenId
	 * 
	 * @param voClasses
	 *            списък от класове, за които трябва да се създадат генератори
	 * 
	 * @throws ApplicationException
	 *             при възникнало изключение при инициализация
	 */
	void initKeys(List<Class<? extends HibernateVO>> voClasses) throws ApplicationException {
		GenIdDAO giDAO = new GenIdDAO();
		try {
			giDAO.beginWork();
			for (Class<? extends HibernateVO> voClass : voClasses) {
				String tableName = voClass.getAnnotation(Table.class).name();
				Criteria crit = giDAO.createCriteria();
				crit.add(Restrictions.eq(GenIdDAO.TABLE_NAME, tableName));
				List<GenId> foundObjs = giDAO.findByCriteria(crit);
				int lastId = foundObjs.isEmpty() ? 0 : foundObjs.get(0).getLastUsedId();
				TableIdGenerator generator = new TableIdGenerator(lastId, tableName);
				this.tableGens.put(voClass, generator);
			}
			giDAO.commitWork();
		} catch (ApplicationException e) {
			giDAO.rollback();
			throw e;
		}
	}

}